﻿/* 
 * FreeLoader, a dependency-resolving loader for JavaScript
 * Copyright (c) 2011 Kai Jäger. Some rights reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the license.txt file. 
 */

(function(scope) {
	var frldr = {};
    
    var currentNamespace = scope;
	
	frldr.ArgumentException = function(argument) {
		this.name = "frldr.ArgumentException";
		this.message = "Invalid value for argument `" + argument + "`";
	};
	frldr.ArgumentException.prototype = new Error();
	
	frldr.ArgumenNullException = function(argument) {
		this.name = "frldr.ArgumentNullException";
		this.message = "Value for argument `" + argument + "` cannot be null";
	};
	frldr.ArgumenNullException.prototype = new Error();
	
	frldr.ClassRedefinitionException = function(qualifiedName) {
		this.name = "frldr.ClassRedefinitionException";
		this.message = "A class with the name `" + qualifiedName + "` already exists";
	};
	frldr.ClassRedefinitionException.prototype = new Error();
	
	frldr.NamespaceRedefinitionException = function(qualifiedName) {
		this.name = "frldr.NamespaceRedefinitionException";
		this.message = "The namespace `" + qualifiedName + "` already exists";
	};
	frldr.NamespaceRedefinitionException.prototype = new Error();
	
	frldr.MissingInitializerException = function(qualifiedName) {
		this.name = "frldr.MissingInitializerException";
		this.message = "Class `" + qualifiedName + "` must have a method `initialize`";
	};
	frldr.MissingInitializerException.prototype = new Error();
    
    frldr.NameNotUniqueException = function(qualifiedName) {
        this.name = "frldr.NameNotUniqueException";
        this.message = "The name `" + qualifiedName + "` is not unique in its current context";
    };
    frldr.NameNotUniqueException.prototype = new Error();
	
    frldr.LoaderException = function(message) {
        this.name = "frldr.LoaderException";
        this.message = message;
    };
    frldr.LoaderException.prototype = new Error();
    
    frldr.DuplicateNameInUseClauseException = function() {
        this.name = "frldr.DuplicateNameInUseClauseException";
        this.message = "Duplicate name in use clause";
    };
    frldr.DuplicateNameInUseClauseException.prototype = new Error();
    
    frldr.MethodNotImplementedException = function(className, name) {
        this.name = "frldr.MethodNotImplementedException";
        this.message = "Class `" + className  + "` must implement abstract method `" + name + "`";
    };
    frldr.MethodNotImplementedException.prototype = new Error();
    
    
	frldr.Assert = {
		argumentIsString: function(name, value) {
			this._checkArgument(name, value, "string");
		},

		argumentIsObject: function(name, value) {
			this._checkArgument(name, value, "object");
		},
        
        argumentIsFunction: function(name, value) {
            this._checkArgument(name, value, "function");
        },
        
        argumentIsNumber: function(name, value) {
            this._checkArgument(name, value, "number");
        },
		
		isTrueForArgument: function(name, value) {
			if (!value) {
				throw new frldr.ArgumentException(name);
			}
		},
		
		_checkArgument: function(name, value, expectedType) {
			if (value === null) {
				throw new frldr.ArgumentNullException(name);
			} else if (typeof(value) != expectedType) {
				throw new frldr.ArgumentException(name);
			}
		}
	};
    
    frldr.Settings = {
        codePath: "./",
        libraryPath: "frldr/",
        applyAll: function(settings) {
            frldr.Assert.argumentIsObject("settings", settings);
            for (var key in settings) {
                this[key] = settings[key];
            }
        }
    };
    
    frldr.mustImplement = {};
    
    frldr.trait = function(name) {
        frldr.Assert.argumentIsString("name", name);
        
        var implementation;
        var traitInclusions;
        
        if (arguments.length >= 3) {
            implementation = arguments[2];
            traitInclusions = arguments[1];
        } else if (arguments.length >= 2) {
            implementation = arguments[1];
            traitInclusions = [];
        } else {
            throw new frldr.ArgumentException("implementation");
        }
        
        frldr.Assert.argumentIsObject("implementation", implementation);
        frldr.Assert.isTrueForArgument("traitInclusions", traitInclusions instanceof Array);
        
        currentNamespace[name] = function() {
            var value;
            for (var field in implementation) {
                this[field] = implementation[field];
            }
            
            var numTraitInclusions = traitInclusions.length;
            for (var i = 0; i < numTraitInclusions; i++) {
                traitInclusions[i].call(this);
            }
        };
    };
    
	frldr.Object = (function() {
		var FLAG_DO_NOT_INITIALIZE = {};
	
		var objectClass = function() {};
		objectClass.subclass = function(name) {
			frldr.Assert.argumentIsString("name", name);
            
            var implementation;
            var traitInclusions;
            
             if (arguments.length >= 3) {
                implementation = arguments[2];
                traitInclusions = arguments[1];
            } else if (arguments.length >= 2) {
                implementation = arguments[1];
                traitInclusions = [];
            } else {
                throw new frldr.ArgumentException("implementation");
            }
            
			frldr.Assert.argumentIsObject("implementation", implementation);
            frldr.Assert.isTrueForArgument("traitInclusions", traitInclusions instanceof Array);
			
			frldr.Assert.isTrueForArgument(name.length > 0, "name");
			
            var qualifiedName = currentNamespace.qualifiedName + "." +  name;

            if (typeof currentNamespace[name] != "undefined") {
				throw new frldr.ClassRedefinitionException(qualifiedName);
			}
            
			var subClass = function(canInitialize) {
				var parentClass = {};
                var numTraitInclusions = traitInclusions.length;
                
                for (var i = 0; i < numTraitInclusions; i++) {
                    traitInclusions[i].call(this);
                }
                
                var field;
                
                for (field in this) {
                    if (this[field] == frldr.mustImplement) {
                        throw new frldr.MethodNotImplementedException(qualifiedName, field);
                    }
                }
                
				for (field in implementation) {
					if (typeof this[field] == "function") {
						parentClass[field] = (function(object, parent, parentFunction) {
							return function() {
								var oldParent = object.parent;
								object.parent = parent;
								var result = parentFunction.apply(object, arguments);
								object.parent = oldParent;
								return result;
							};
						})(this, this.parent, this[field]);
					}
					this[field] = implementation[field];
				}
				
				this.parent = parentClass;
                
				if (canInitialize !== FLAG_DO_NOT_INITIALIZE &&
					typeof this.initialize == "function") {
					this.initialize.apply(this, arguments);
				}
			};
			
			subClass.prototype = new this(FLAG_DO_NOT_INITIALIZE);
			subClass.subclass = objectClass.subclass;
			
			if (typeof implementation.initialize != "function" &&
				typeof subClass.prototype.initialize == "function" &&
				subClass.prototype.initialize.arity > 0) {
				throw new frldr.MissingInitializerException(name);
			}
			
			currentNamespace[name] = subClass;
		};
		
		return objectClass;
	})();
    
    frldr.Loader = (function() {
        var JAVASCRIPT_FILE_EXTENSION = ".js";
        var FRLDR_NAMESPACE_PREFIX = "frldr.";
    
        var loaderClass = function(baseUri) {        
            frldr.Assert.argumentIsString("baseUri", baseUri);
            frldr.Assert.isTrueForArgument("baseUri", baseUri.length > 0);
        
            this.baseUri = baseUri;
            
            if (baseUri.charAt(baseUri.length - 1) != "/") {
                baseUri += "/";
            }
            
            this.namespaces = {};
            this._pendingLoads = 0;
            this._pendingCallbacks = [];
            
            loaderClass._loaders.push(this);
        };
        
        loaderClass.prototype = new frldr.Object();
        
        loaderClass.prototype.load = function(qualifiedName, callback) {
            frldr.Assert.argumentIsString("qualifiedName", qualifiedName);
            this.loadAll([qualifiedName], callback);    
        };
        
        loaderClass.prototype.loadAll = function(qualifiedNames, callback) {
            frldr.Assert.isTrueForArgument("qualifiedNames", qualifiedNames instanceof Array);
        
            var numNames = qualifiedNames.length;            
            var lookUpTable = {};            
            
            var hasCallback = typeof callback == "function";            
            
            var unmetDependencies = numNames;
            var currentName;
            for (var i = 0; i < numNames; i++) {
                currentName = qualifiedNames[i];
                frldr.Assert.argumentIsString("qualifiedNames", currentName);
                if (typeof lookUpTable[currentName] != "undefined") {
                    throw new frldr.NameNotUniqueException(currentName);
                } else {
                    lookUpTable[currentName] = true;                    
                    if (typeof this.namespaces[currentName] == "object") {
                        --unmetDependencies;
                    } else {
                        this._enqueue(currentName);
                    }
                }
            }
            
            if (hasCallback) {
                if (unmetDependencies < 1) {
                    callback();
                } else {
                    this._pendingCallbacks.push(callback);
                }
            }
        };
        
        loaderClass.prototype._enqueue = function(qualifiedName) {
            var scriptUri;
            var pathName = qualifiedName.replace(/\./g, "/") + JAVASCRIPT_FILE_EXTENSION;
            if (qualifiedName.length >= FRLDR_NAMESPACE_PREFIX.length &&
                qualifiedName.substring(0, FRLDR_NAMESPACE_PREFIX.length) == FRLDR_NAMESPACE_PREFIX) {
                scriptUri = frldr.Settings.libraryPath + pathName;    
            } else {
                scriptUri = this.baseUri + pathName;
            }
            this.namespaces[qualifiedName] = null;
            ++this._pendingLoads;
            this._createScriptTag(scriptUri, function() {
               throw new frldr.LoaderException("Failed to load `" + qualifiedName + "` from URI `" + scriptUri + "`");
            });
        };
        
        loaderClass.prototype._createScriptTag = function(uri, failCallback) {
            var scriptTag = document.createElement("script");
            scriptTag.setAttribute("type", "text/javascript");
            scriptTag.setAttribute("src", uri);            
            scriptTag.onerror = failCallback;
            document.body.appendChild(scriptTag);
        };
        
        loaderClass.prototype._scriptLoaded = function(qualifiedName, namespace) {
            if (typeof this.namespaces[qualifiedName] == "undefined") {
                return;
            }
            
            --this._pendingLoads;
            
            this.namespaces[qualifiedName] = namespace;
            
            if (this._pendingLoads < 1) {
                var callback;
                while (this._pendingCallbacks.length > 0) {
                    callback = this._pendingCallbacks.pop();
                    callback();
                }                
            }
        };
        
        loaderClass._loaders = [];
        loaderClass._notifyScriptLoaded = function(qualifiedName, namespace) {
            var numLoaders = loaderClass._loaders.length;
            for (var i = 0; i < numLoaders; i++) {
                loaderClass._loaders[i]._scriptLoaded(qualifiedName, namespace);
            }
        };
        
        loaderClass.defaultLoader = new loaderClass(frldr.Settings.codePath);
        
        loaderClass.subclass = frldr.Object.subclass;
        
        return loaderClass;
    })();    
    
	frldr.Namespace = (function() {
        var namespaceClass = function(qualifiedName) {
            frldr.Assert.argumentIsString("qualifiedName", qualifiedName);
        
            this.qualifiedName = qualifiedName;
            
            var nameParts = qualifiedName.split(".");
            var numParts = nameParts.length;
            frldr.Assert.isTrueForArgument(numParts > 0, "qualifiedName");
                
            var currentNamespace = scope;
            var newNamespace;
            var currentPart;
                
            for (var i = 0; i < numParts; i++) {
                currentPart = nameParts[i];
                frldr.Assert.isTrueForArgument(currentPart.length > 0, "qualifiedName");
                if (typeof currentNamespace[currentPart] == "undefined") {
                    if (i == numParts - 1) {
                        newNamespace = this;
                    } else {
                        newNamespace = {};
                    }
                    currentNamespace[currentPart] = newNamespace;
                    currentNamespace = newNamespace;
                } else {
                    if (i == numParts - 1) {
                        throw new frldr.NamespaceRedefinitionException(qualifiedName);
                    }
                    currentNamespace = currentNamespace[currentPart];
                }
            }
            
            this.dependencies = [];
        };
        
        namespaceClass.prototype = new frldr.Object();
        
        namespaceClass.prototype.uses = function() {
            var numDependencies = arguments.length;
            for (var i = 0; i < numDependencies; i++) {
                frldr.Assert.argumentIsString(i + 1, arguments[i]);
                frldr.Assert.isTrueForArgument(i + 1, arguments[i].length > 0);
                this.dependencies.push(arguments[i]);
            }
            return this;
        };
        
        namespaceClass.prototype.is = function(body) {
            frldr.Assert.argumentIsFunction("body", body);
            
            var thisNamespace = this;
            var callback = function() {
                var oldNamespace = currentNamespace;
                currentNamespace = thisNamespace;
                body();
                currentNamespace = oldNamespace;
            };
            
            if (this.dependencies.length > 0) {
                try {
                    frldr.Loader.defaultLoader.loadAll(this.dependencies, callback);
                } catch (e) {
                    if (e instanceof frldr.NameNotUniqueException) {
                        throw new frldr.DuplicateNameInUseClauseException();
                    } else {
                        throw e;
                    }
                }
            } else {
                callback();
            }
            
            frldr.Loader._notifyScriptLoaded(this.qualifiedName, this);
        };
        
        namespaceClass.subclass = frldr.Object.subclass;
        
        currentNamespace = scope;
        
        frldr.namespace = function(qualifiedName) {        
            return new frldr.Namespace(qualifiedName);
        };	
        
        return namespaceClass;
    })();
    
    frldr.Delegate = {
        create: function(object, method) {
            return function() {
                return method.call(object, arguments);
            };  
        }
    };
    
	scope.frldr = frldr;
})(window);